/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. See accompanying LICENSE file.
 */
package com.hortonworks.registries.auth.util;

import com.hortonworks.registries.auth.client.AuthenticationException;

import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

/**
 */
public class AuthToken implements Principal {

    /**
     * Constant that identifies an anonymous request.
     */

    private static final String ATTR_SEPARATOR = "&";
    private static final String USER_NAME = "u";
    private static final String PRINCIPAL = "p";
    private static final String EXPIRES = "e";
    private static final String TYPE = "t";

    private final static Set<String> ATTRIBUTES =
            new HashSet<String>(Arrays.asList(USER_NAME, PRINCIPAL, EXPIRES, TYPE));

    private String userName;
    private String principal;
    private String type;
    private long expires;
    private String tokenStr;

    protected AuthToken() {
        userName = null;
        principal = null;
        type = null;
        expires = -1;
        tokenStr = "ANONYMOUS";
        generateToken();
    }

    private static final String ILLEGAL_ARG_MSG = " is NULL, empty or contains a '" + ATTR_SEPARATOR + "'";

    /**
     * Creates an authentication token.
     *
     * @param userName user name.
     * @param principal principal (commonly matches the user name, with Kerberos is the full/long principal
     * name while the userName is the short name).
     * @param type the authentication mechanism name.
     * (<code>System.currentTimeMillis() + validityPeriod</code>).
     */
    public AuthToken(String userName, String principal, String type) {
        checkForIllegalArgument(userName, "userName");
        checkForIllegalArgument(principal, "principal");
        checkForIllegalArgument(type, "type");
        this.userName = userName;
        this.principal = principal;
        this.type = type;
        this.expires = -1;
    }

    /**
     * Check if the provided value is invalid. Throw an error if it is invalid, NOP otherwise.
     *
     * @param value the value to check.
     * @param name the parameter name to use in an error message if the value is invalid.
     */
    protected static void checkForIllegalArgument(String value, String name) {
        if (value == null || value.length() == 0 || value.contains(ATTR_SEPARATOR)) {
            throw new IllegalArgumentException(name + ILLEGAL_ARG_MSG);
        }
    }

    /**
     * Sets the expiration of the token.
     *
     * @param expires expiration time of the token in milliseconds since the epoch.
     */
    public void setExpires(long expires) {
        this.expires = expires;
        generateToken();
    }

    /**
     * Returns true if the token has expired.
     *
     * @return true if the token has expired.
     */
    public boolean isExpired() {
        return getExpires() != -1 && System.currentTimeMillis() > getExpires();
    }

    /**
     * Generates the token.
     */
    private void generateToken() {
        StringBuffer sb = new StringBuffer();
        sb.append(USER_NAME).append("=").append(getUserName()).append(ATTR_SEPARATOR);
        sb.append(PRINCIPAL).append("=").append(getName()).append(ATTR_SEPARATOR);
        sb.append(TYPE).append("=").append(getType()).append(ATTR_SEPARATOR);
        sb.append(EXPIRES).append("=").append(getExpires());
        tokenStr = sb.toString();
    }

    /**
     * Returns the user name.
     *
     * @return the user name.
     */
    public String getUserName() {
        return userName;
    }

    /**
     * Returns the principal name (this method name comes from the JDK {@link Principal} interface).
     *
     * @return the principal name.
     */
    @Override
    public String getName() {
        return principal;
    }

    /**
     * Returns the authentication mechanism of the token.
     *
     * @return the authentication mechanism of the token.
     */
    public String getType() {
        return type;
    }

    /**
     * Returns the expiration time of the token.
     *
     * @return the expiration time of the token, in milliseconds since Epoc.
     */
    public long getExpires() {
        return expires;
    }

    /**
     * Returns the string representation of the token.
     * <p>
     * This string representation is parseable by the {@link #parse} method.
     *
     * @return the string representation of the token.
     */
    @Override
    public String toString() {
        return tokenStr;
    }

    public static AuthToken parse(String tokenStr) throws AuthenticationException {
        if (tokenStr.length() >= 2) {
            // strip the \" at the two ends of the tokenStr
            if (tokenStr.charAt(0) == '\"' &&
                    tokenStr.charAt(tokenStr.length() - 1) == '\"') {
                tokenStr = tokenStr.substring(1, tokenStr.length() - 1);
            }
        }
        Map<String, String> map = split(tokenStr);
        // remove the signature part, since client doesn't care about it
        map.remove("s");

        if (!map.keySet().equals(ATTRIBUTES)) {
            throw new AuthenticationException("Invalid token string, missing attributes");
        }
        long expires = Long.parseLong(map.get(EXPIRES));
        AuthToken token = new AuthToken(map.get(USER_NAME), map.get(PRINCIPAL), map.get(TYPE));
        token.setExpires(expires);
        return token;
    }

    /**
     * Splits the string representation of a token into attributes pairs.
     *
     * @param tokenStr string representation of a token.
     *
     * @return a map with the attribute pairs of the token.
     *
     * @throws AuthenticationException thrown if the string representation of the token could not be broken into
     * attribute pairs.
     */
    private static Map<String, String> split(String tokenStr) throws AuthenticationException {
        Map<String, String> map = new HashMap<String, String>();
        StringTokenizer st = new StringTokenizer(tokenStr, ATTR_SEPARATOR);
        while (st.hasMoreTokens()) {
            String part = st.nextToken();
            int separator = part.indexOf('=');
            if (separator == -1) {
                throw new AuthenticationException("Invalid authentication token");
            }
            String key = part.substring(0, separator);
            String value = part.substring(separator + 1);
            map.put(key, value);
        }
        return map;
    }

}
